
# Defines the game building elements of the game:
# Rooms, Props and Characters

import pygame, weakref
from pygame import transform, Rect
from xml.etree import ElementTree as xml

from sprites import Sprite, Target
from interface import Label
from library import Resource, xml_map, State
from utils import indentify
from event import Event
 
# A list of common angles
angle ={'N':360, 'W': 90, 'S':180, 'E':270, 'NW':45, 'SW':135, 'SE':225, 'NE':315}    


# Defines an object which can load its data from an XML node
class Element(object):
    
    def __init__(self, node):

        # Parse node if not already parsed
        if isinstance(node, basestring): 
            try:
                node = xml.XML(node)
            except Exception, error: 
                print "Parser exception on line:",  error.lineno
                print '>> "', node.splitlines()[error.lineno - 1], '"'
                raise
        
        object.__init__(self)
        self.source = node; self.id = None
        
        # Load all attributes directly into the internal dict
        # Attributes which are only digits are converted to ints
        self.text = node.text
        self.__dict__.update((k, (int(v) if v.isdigit() else v)) for k, v in node.attrib.items())

        
        # Load all children nodes into lists by their tags
        for child in node:
            if child.tag in self: self.__dict__[child.tag].append(child)
            else: self.__dict__[child.tag] = [child]

        # Parse element bound events
        events = Event.from_XML(self.event) if 'event' in self else list()
        self.event = dict()
        self.add_event(events)

        
    # This method returns true if the given string was loaded into the element
    def __contains__(self, key): return key in self.__dict__
            
    # Default id printing for elements
    def __str__(self): return '<' + self.source.tag + ': ' + str(self.id) + '>'

    # Add any event (or event list) to a any given event trigger
    def add_event(self, event_ls, trigger = None, cond = None):
        
        # Make sure arguments are in the correct format
        if trigger is not None:
            assert isinstance(trigger, basestring), "Trigger must be a string ID"
        if not isinstance(event_ls, list): event_ls = [event_ls]
        
        # If no events to append, just quit the method 
        if not event_ls: return
        if trigger is not None and trigger not in self.event:
            self.event[trigger] = []
        
        # Add each hook in the list to the specified trigger
        for event in event_ls:
            assert isinstance(event, Event), "Invalid event: " + str(event)
            key = event.trigger if trigger is None else trigger
            if cond is not None:
                event.condition = str(cond)
            event.owner = weakref.proxy(self)
            if key not in self.event:
                self.event[key] = [event]
            else:
                self.event[key].append(event)

        

# A structural object that loads its characteristics from a mapfile
class Room(Element):
    
    # Meta-room local variable dict
    state_storage = {}
    
    # Define possible room content types
    content_type = ("exit", "prop")
        
    # Required data for room loading must be in the form of a XML Element
    def __init__(self, data):
        
        # Default containers
        self.exit = []; self.prop = []; self.npc = []
        
        # Verify tag validity and load to memory
        Element.__init__(self, data)
        
        # Set visual room parameters
        self.bg = Resource(self.bg)
        
        # Load room state; load defaults if required
        self.state = State(Room.state_storage, self.id)
        if 'first' not in self.state: 
            self.state['first'] = True
            self.prop = [Prop(self, p) for p in self.prop]
        else:
            pass
            #self.prop = Container.retrieve(self.id)
        self.state['here'] = weakref.proxy(self)
        
        # Load room contents
        self.exit = [Exit(i) for i in self.exit]
        
                 
        # Put everything into its starting position
        self.arrange()

        print "State: ", self.state
        
    # Move all exits and props back to their original position
    def arrange(self):
        
        # Accomodate the exit's positions
        l = 0; r = 0
        sw, sh = pygame.display.get_surface().get_size()
        
        if len(self.exit) >= 1: 
            h = int(self.exit[0].rect.height)
            w = sw - (20 + h)

            self.exit.sort(key = lambda x: x.dir)
            for e in self.exit:
                if e.dir > 180 and e.dir <= 360:
                    r += 1; e.rect.topleft = (w, sh- int(h * r * 1.3))

            self.exit.sort(key = lambda x: x.dir * -1)
            for e in self.exit:
                if e.dir > 0 and e.dir <= 180: 
                    l += 1; e.rect.topleft = (20, sh - int(h * l * 1.3))
                    
        # Accomodate the prop's positions
                
            
                
    # Cleanup the room by removing all contained objects
    def __del__(self):
        self.state['first'] = False
        Prop.group.empty()
        del self.state['here']
        for n in Room.content_type:
            for i in self.__dict__[n]: i.remove()
            
    # Return true if this room was never visited previously
    def first_visit(self): return self.id not in Room.state_storage
    
    
    # Search for anything contained within the room, by any property of that element
    # This method will return either a weakref.proxy or None
    def find(self, cls, val, key="id"):
        cls = cls.lower()
        assert cls in Room.content_type, "Can only search sub-elements"
        val = val.lower(); key = key.lower()
        for l in self.__dict__[cls]:
            if l.__dict__[key].lower() == val: return weakref.proxy(l)
            
            
# Defines a special room object that warps the player to another room
class Exit(Element, Target):
    
    # A sprite group of all exits that require rendering
    group = pygame.sprite.Group()
    transparency = 90
    
    # Set to a string when a warp has ben requested
    trigger = None
    
    # Remember disable exits
    __memory = None; __priority = False
        
    def __init__(self, data):
        
        # Initialize the Exit element
        Element.__init__(self, data)
                
        # Initialize the Target system
        Target.__init__(self, image = Resource("exit.png"), id= self.dest, vgroup = Exit.group)
        
        # Adjust transparency of the image
        self.translucify(Exit.transparency)
        
        # Rotate sprite according to given direction
        if isinstance(self.dir, basestring): self.dir = angle[self.dir.upper()]
        if self.dir != 0: 
            rect = self.image.get_rect()
            img = transform.rotozoom(self.image, self.dir, 1)
            size = (img.get_width() - rect.width) // 2, (img.get_height() - rect.height) // 2
            self.image = transform.chop(img, Rect((0,0), size))
        
        # Set mouse over tooltip
        if self.text: 
            self.mouseover = Label(self.text, target = self, show = False)
            
        
    def click(self, pos):
        self.leave(); Exit.trigger = self.dest
                
    # Just some eye-candy
    def hover(self): 
        self.translucify(255); super(Exit, self).hover()
        
    def leave(self): 
        self.translucify(Exit.transparency); super(Exit, self).leave()
                
    # Default id printing for exits (based on destination, not on id)
    def __str__(self): return '<' + self.source.tag + ': ' + self.dest + '>'
    
    # Sends all exits offscreen so that the player may not interact with them
    @classmethod
    def disable(cls, priority=True):
        if cls.__memory is not None: return
        cls.__memory = list()
        width = pygame.display.get_surface().get_width()
        for exit in cls.group:
            cls.__memory.append((weakref.ref(exit), exit.rect.center))
            if exit.rect.centerx >= width // 2: 
                exit.rally((width+exit.rect.width, None), 150)
            else: exit.rally((-exit.rect.width*2, None), 150)
            
    # Returns all disabled exits onscreen
    @classmethod
    def enable(cls, priority=True):
        if cls.__memory is None: return
        
        while cls.__memory:
            exit, point = cls.__memory.pop()
            exit = exit()
            if exit is not None and exit in cls.group:
                exit.rally(point, 150)        
           
                            
# A structural object that represents a user manipulable item
# All Props must be asociated to a Container in order to be visible.
# A Prop may be based on an previously defined item type or be unique.
class Prop(Target, Element):

    # A sprite group of all props that require rendering
    group = pygame.sprite.Group()
    
    def __init__(self, data):
        
        # Initialize the Exit element
        Element.__init__(self, data)
        pickup = True if 'pickup' in self and eval(self.pickup) else False
        name = self.id if 'id' in self else None
                
        # Initialize the Target system
        Target.__init__(self, image = self.img, id=name,  \
            vgroup = Prop.group, drag = pickup)

        # Set mouse over tooltip
        if self.text: 
            self.mouseover = Label(self.text, target = self, show = False)

        # Set initial position
        if 'pos' in self: self.rect.center = [int(i.strip()) for i in self.pos.split(',')]
        if 'hidden' in self and eval(self.hidden): self.hide()

    
# A container is a basic element that can hold Prop elements 
class Container(Target, Element):
    
    def __init__(self, room, data):
        # A container may be optionaly asociated with a room
        self.room = room if room else None
        Element.__init__(self, data)

        # Initialize container grid
        if 'grid' not in self: self.grid = "1x1"
        self.grid = [int(i) for i in self.grid.split('x')]
        self.cells = [[0] * self.grid[1]] * self.grid[0]

        # Set cell size defaults
        assert 'cell' in self, "Specify a cell size for the container"
        self.cell = [int(i.trim()) for i in self.cell.split(',')]
        
        # Internal prop/item list 
        self.items = []

        # Determine size of the Target area
        width = self.grid[0] * self.cell[0]
        height = self.grid[1] * self.cell[1]

        # Initial container Position
        if 'pos' not in self: self.pos = "0,0"
        px, py = [int(i.trim()) for i in self.pos.split(',')]
        
        # If an image was specified for this container, it must be the same
        # size or larger than the actual container grid
        rect = rectify(px, py, width, height)
        # Target.__init__(self, image = self.img, 
             
        # Set default alignment
        if 'align' not in self or self.align not in ('NE', 'SE', 'SW', 'NW'):
            self.align = 'NE'
        
        
        Target.__init__(self, **kwargs)
        self.grid_width = grid[0]
        self.grid_height = grid[1]
        self.content_dict = dict()
        self.content_grid =list()

    # Serializes the current container, unloading 
        

# The most important object that can appear in a room: A NPC
class NPC(Target, Element):
    
    # A sprite group of all onscreen NPCs
    group = pygame.sprite.Group()
    
    # Meta variable to hold NPC states
    state_storage = dict()
    
    
    def __init__(self, data):
        
        # Initialize the elements
        Element.__init__(self, data)
        
        # Initialize defined npc moods
        self.pose = dict((p.attrib['id'], p.attrib) for p in self.pose)
        
        # Initialize the Target system
        Target.__init__(self, id = self.id, vgroup = NPC.group, show = False)
        
        # Initialize NPC data state
        self.state = State(NPC.state_storage, self.id)
        if 'first' not in self.state: 
            self.state['first'] = True
            
        # Initialize NPC sprite face
        self.watch('rect', self.update_face)
        self.face = self.rect
        
        # Initialize starting npc mood
        assert 'base' in self.__dict__, "No base mood for NPC " + str(self.id)
        self.__mood = dict()
        self.set_mood(self.base)
                
        # If location is set to None, NPC is nowhere on the map
        # oterwise, it is a string id for the room where the NPC is located
        self.location = None
        self.rect.left = 400

    # Switch the NPCs current mood pose
    def set_mood(self, mood):
        
        # Verify that we are switching to a valid new mood
        assert mood in self.pose, "Invalid mood for this NPC"
        if self.mood == mood: return
        
        # Make the actual image switch
        mood = self.__mood = self.pose[mood]
        self.image = Resource(mood['img'])
        self.state['mood'] = mood['id']
        
        
    # See what is the current mood
    def get_mood(self):
        return self.__mood.get('id')
    
    mood = property(get_mood, set_mood, None)
    
    # Update the NPCs face and other stuff
    def update_face(self, attribute, rect, sender):
        if rect is not None:
            face = Rect(rect)
            face.top += self.face.height // 8
            face.height /= 4
            face.left += self.face.width // 12
            face.width /= 6
            self.face = self.notify('face', face)
        
    
    # Print the NPC name
    def __str__(self):
        return self.id.title()
    
        


        
        
        

